import CryptoJS from "crypto-js";
import CryptoSM3 from "./sm3.js";

class FileHashUtil {
  hashTypeArg: string[] = [];
  chunkSize: number = 1024 * 1024 * 128;
  enabledAlgorithms: { name: string; instance: any }[] = [];
  onHashLoadStart?: (file: File, hashTypes: string[], source?: any) => void;
  onHashProgress?: (file: File, percentage: number, hashTypes: string[], source?: any) => void;
  onHashLoadEnd?: (file: File, results: any[], hashTypes: string[], source?: any) => void;

  algorithms: { name: string; type: any; param?: { outputLength: number } }[] = [];

  constructor(setting: Partial<FileHashUtil> = {}) {
    Object.assign(
      this,
      {
        hashTypeArg: ["SM3", "SHA256"],
        chunkSize: 1024 * 1024 * 128, // 128M at a time
        onHashLoadStart: null,
        onHashProgress: null,
        onHashLoadEnd: null
      },
      setting
    );

    this.algorithms = [
      { name: "MD5", type: CryptoJS.algo.MD5 },
      { name: "SM3", type: CryptoSM3 },
      { name: "SHA1", type: CryptoJS.algo.SHA1 },
      { name: "SHA256", type: CryptoJS.algo.SHA256 },
      { name: "SHA512", type: CryptoJS.algo.SHA512 },
      { name: "SHA3-224", type: CryptoJS.algo.SHA3, param: { outputLength: 224 } },
      { name: "SHA3-256", type: CryptoJS.algo.SHA3, param: { outputLength: 256 } },
      { name: "SHA3-384", type: CryptoJS.algo.SHA3, param: { outputLength: 384 } },
      { name: "SHA3-512", type: CryptoJS.algo.SHA3, param: { outputLength: 512 } },
      { name: "RIPEMD-160", type: CryptoJS.algo.RIPEMD160 }
    ];
  }

  compatible(): boolean {
    try {
      if (typeof FileReader === "undefined") return false;
      if (typeof Blob === "undefined") return false;
      const blob = new Blob();
      if (!blob.slice && !("webkitSlice" in blob)) return false;
    } catch (e) {
      return false;
    }
    return true;
  }

  initEnableAlgorithms(): { name: string; instance: any }[] {
    const enabledAlgorithms: { name: string; instance: any }[] = [];
    for (const hashType of this.hashTypeArg) {
      for (const algo of this.algorithms) {
        if (hashType === algo.name) {
          const algoInst = { name: algo.name, instance: algo.type.create(algo.param ?? {}) };
          enabledAlgorithms.push(algoInst);
          break;
        }
      }
    }
    return enabledAlgorithms;
  }

  hash(file: File, source = null, pos = 0): Promise<string[]> {
    const _this = this;
    if (pos === 0) {
      this.enabledAlgorithms = this.initEnableAlgorithms();
      if (_this.onHashLoadStart) {
        _this.onHashLoadStart(file, _this.hashTypeArg, source);
      }
    }
    return new Promise((resolve) => {
      const end = Math.min(pos + _this.chunkSize, file.size);
      const fileReader = new FileReader();
      fileReader.onload = (e: any) => {
        if (_this.onHashProgress) {
          const percentage = Math.ceil((pos * 100) / file.size);
          _this.onHashProgress(file, percentage, _this.hashTypeArg, source);
        }
        const newPos = end;
        const wordArray = _this.arrayBufferToWordArray(e.target.result as ArrayBuffer);
        for (const algo of _this.enabledAlgorithms) {
          if (algo.instance instanceof CryptoSM3) {
            algo.instance.update(new Uint8Array(e.target.result as ArrayBuffer));
          } else {
            algo.instance.update(wordArray);
          }
        }
        if (newPos < file.size) {
          _this.hash(file, source, newPos).then(resolve);
        } else {
          const result = _this.enabledAlgorithms.map((a) => a.instance.finalize());
          const hex = result.map((r) => r.toString());
          if (_this.onHashLoadEnd) {
            _this.onHashLoadEnd(file, result, _this.hashTypeArg, source);
          }
          resolve(hex);
        }
      };
      const sliceMethod =
        "webkitSlice" in file
          ? (file as { webkitSlice: (start: number, end: number) => Blob }).webkitSlice
          : undefined;
      const blob = sliceMethod ? sliceMethod.call(file, pos, end) : file.slice(pos, end);
      fileReader.readAsArrayBuffer(blob);
    });
  }

  arrayBufferToWordArray(arrayBuffer: ArrayBuffer): any {
    const fullWords = Math.floor(arrayBuffer.byteLength / 4);
    const bytesLeft = arrayBuffer.byteLength % 4;

    const u32 = new Uint32Array(arrayBuffer, 0, fullWords);
    const u8 = new Uint8Array(arrayBuffer);

    const cp = [];
    for (let i = 0; i < fullWords; ++i) {
      cp.push(this.swapendian32Hash(u32[i]));
    }
    if (bytesLeft) {
      let pad = 0;
      for (let i = bytesLeft; i > 0; --i) {
        pad = pad << 8;
        pad += u8[u8.byteLength - i];
      }

      for (let i = 0; i < 4 - bytesLeft; ++i) {
        pad = pad << 8;
      }

      cp.push(pad);
    }

    return CryptoJS.lib.WordArray.create(cp, arrayBuffer.byteLength);
  }

  swapendian32Hash(val: number): number {
    return (
      (((val & 0xff) << 24) |
        ((val & 0xff00) << 8) |
        ((val >> 8) & 0xff00) |
        ((val >> 24) & 0xff)) >>>
      0
    );
  }
}

export default FileHashUtil;
